CooldownWatch = {}

local CooldownWatch = CooldownWatch
local db

local frame, frameByName, inventoryframe, frameBySlot, seconds, minutes

frame = {}
frameByName = {}
inventoryframe = {}
frameBySlot = {}
seconds, minutes = {}, {}
for i=1,120 do
	tinsert(seconds, i.."s")
end
for i=1,120 do
	tinsert(minutes, i.."m")
end

CooldownWatch.frame = frame


----------------------------------------------------------------------------
-- Module system
-- Modules define table entries of CooldownWatch, mostly functions.
-- A module may be located in an addon, in this case the addon is loaded
-- when the table entry is indexed.
-- Each module is initialized with "private" data, so that this data may 
-- be used as upvalues.
----------------------------------------------------------------------------


local modules = {
	Config = {
		addon = "CooldownWatchConfig",
		data = {
			"RemoveWatch",
			"RemoveAll",
			'MoveFrame',
			'GetConfigMode',
			'ToggleConfigMode',
			'ConfigMode',
			'RecreateAll',
		},	
		status = 'not loaded',
		dependencies = {'InitConfig'},
	},
	Init = {
		data = {
			"LoadData",
			"UnloadInit",
			"FirstTime",
			"Initialize",
			"Enable",
			"UpdateLoadData",
		},
		status = 'load',
		dependencies = {'InitConfig'},
	},
	InitConfig = {
		data = {
			--[[
			"AddWatch",
			"CreateWatchFrame",
			]]
		},
		status = 'load',
	},
}
CooldownWatch.modules = modules

local function initialize(modulename)
	local module = modules[modulename]
	if module.dependencies then
		for _,depname in ipairs(module.dependencies) do
			initialize(depname)
		end
	end
	if module.status =='loaded' then
		if type(module.init) == "function" then
			--CooldownWatch:Print("initializing module "..modulename)
			module.init(frame, frameByName, inventoryframe, frameBySlot, seconds, minutes)
			module.init = nil
		else
			--CooldownWatch:Print("couldn't initialize module "..modulename)
		end
		module.status = 'initialized'
	end
end

local function __index(table, key)
	for modulename, module in pairs(modules) do
		for _, entry in pairs(module.data) do
			if entry == key then
				-- load if necessary
				if module.addon and module.status=='not loaded' then
					if module.dependencies then
						for _,dep in ipairs(module.dependencies) do
							if modules[dep].status=='not loaded' then
								modules[dep].status='load'
								--CooldownWatch:Print("dependency "..dep)
							end
						end
					end
					CooldownWatch:Print("loading addon "..module.addon)
					module.status = 'load'

					LoadAddOn(module.addon)
				end
				if module.status == 'load' then
					module.status = 'loaded'
				end
				if module.dependencies then
					for _,dep in ipairs(module.dependencies) do
						if modules[dep].status=='load' then
							modules[dep].status='loaded'
						end
					end
				end
				initialize(modulename)
				return rawget(table, key)
			end
		end
	end
end
setmetatable(CooldownWatch, {__index=__index})


----------------------------------------------------------------------------
-- List layouts
----------------------------------------------------------------------------
CooldownWatch.listlayout = {}
do
	local spacing = 2
	function CooldownWatch:SetSpacing(value)
		db.spacing = value
		spacing = value
	end
	do
		local Expand = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPLEFT", self.anchor, "BOTTOMLEFT", 0, -spacing)
		end
		local Collapse = function(self)
			self:ClearAllPoints()
			self:SetPoint("BOTTOMLEFT", self.anchor, "BOTTOMLEFT")
		end
		CooldownWatch.listlayout['TOP_BOTTOM'] = { Collapse = Collapse, Expand = Expand }
	end
	do
		local Expand = function(self)
			self:ClearAllPoints()
			self:SetPoint("BOTTOMLEFT", self.anchor, "TOPLEFT", 0, spacing)
		end
		local Collapse = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPLEFT", self.anchor, "TOPLEFT")
		end
		CooldownWatch.listlayout['BOTTOM_TOP'] = { Collapse = Collapse, Expand = Expand }
	end
	do
		local Expand = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPLEFT", self.anchor, "TOPRIGHT", spacing, 0)
		end
		local Collapse = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPRIGHT", self.anchor, "TOPRIGHT")
		end
		CooldownWatch.listlayout['LEFT_RIGHT'] = { Collapse = Collapse, Expand = Expand }
	end
	do
		local Expand = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPRIGHT", self.anchor, "TOPLEFT", -spacing, 0)
		end
		local Collapse = function(self)
			self:ClearAllPoints()
			self:SetPoint("TOPLEFT", self.anchor, "TOPLEFT")
		end
		CooldownWatch.listlayout['RIGHT_LEFT'] = { Collapse = Collapse, Expand = Expand }
	end
end

-- TODO
local develop = nil

--[[
interesting event sequences:

If a spell is used:
UNIT_SPELLCAST_SUCCEEDED("player", spellname)
UNIT_SPELLCAST_STOP("player", spellname)
ACTIONBAR_UPDATE_COOLDOWN
SPELL_UPDATE_COOLDOWN


If an equippable item is used:
UNIT_SPELLCAST_SUCCEEDED("player", buffname) buffname is the name of the trinket specific buff.
SPELL_UPDATE_COOLDOWN
BAG_UPDATE_COOLDOWN()

If an item is equipped
ITEM_LOCK_CHANGED(bag,slot) lock the bag item
SPELL_UPDATE_COOLDOWN()
BAG_UPDATE_COOLDOWN()
UNIT_INVENTORY_CHANGED("player")
ITEM_LOCK_CHANGED(slot) lock the inventory item

If an item is unequipped
?

If an unequippable item is used
UNIT_SPELLCAST_SUCCEEDED("player", spellname) spellname seems to be specific to the item type, and is not the item name
SPELL_UPDATE_COOLDOWN()
BAG_UPDATE_COOLDOWN()
]]

-- Checks all items. Called on BAG_UPDATE_COOLDOWN.
function CooldownWatch.CheckItems(listener)
	for i=1,#inventoryframe do
		inventoryframe[i].CheckCooldown()
	end
end

-- Checks an inventory slot. Called on ITEM_LOCK_CHANGED.
function CooldownWatch.CheckInventory(listener, event, slot1, slot2)
	if slot2 then return end -- It was a bag update.
	local f = frameBySlot[slot1]
	if f then
		f.CheckCooldown()
		if f.icon then
			local tex = f.GetTexture(f.id, f.idtype)
			f.icon:SetTexture(tex)
		end
	end
end

-- Tells a spell frame to update on SPELL_UPDATE_COOLDOWN. Called on UNIT_SPELLCAST_SUCCEEDED. 
function CooldownWatch.CheckSpell(listener, event, unitid, spellname)
	if unitid~="player" and unitid~='pet' then return end
	local cooldownframe = frameByName[spellname]
	if cooldownframe then
		cooldownframe:RegisterEvent("SPELL_UPDATE_COOLDOWN")
	end
end

do
	local equipListener,spellListener,itemListener
	local listening
	function CooldownWatch:StartEventListening()
		if not equipListener then
			equipListener = CreateFrame("Frame",nil,CooldownWatchFrame)
			equipListener:SetScript("OnEvent", CooldownWatch.CheckInventory)
			spellListener = CreateFrame("Frame",nil,CooldownWatchFrame)
			spellListener:SetScript("OnEvent", CooldownWatch.CheckSpell)
			itemListener = CreateFrame("Frame",nil,CooldownWatchFrame)
			itemListener:SetScript("OnEvent", CooldownWatch.CheckItems)
		end
		equipListener:RegisterEvent("ITEM_LOCK_CHANGED")
		spellListener:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
		itemListener:RegisterEvent("BAG_UPDATE_COOLDOWN")
		listening = true
	end
	function CooldownWatch:StopEventListening()
		if listening then
			equipListener:UnregisterEvent("ITEM_LOCK_CHANGED")
			spellListener:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
			itemListener:UnregisterEvent("BAG_UPDATE_COOLDOWN")
		end
		listening = nil
	end
end

-- Initialize must not be part of the Init module, because the SavedVar is initialized here.
-- This must happen before any module is loaded.
function CooldownWatch:Initialize()
	local firstTime
	if not CooldownWatchVar then
		CooldownWatchVar = {
			version = 8,
			width = 108,
			height = 18,
			spacing = 2,
			statusbarcolor = {1,1,1,0.3},
			backdropcolor = {1,1,1,0.1},
			textcolor = {1,1,1,1},
			textshadowcolor = {0,0,0,1},
			bartexture = [[Interface\Addons\CooldownWatch\Smooth]],
			layout = 'TOP_BOTTOM',
			font = "Fonts\\FRIZQT__.TTF",
			fontsize = 10,
			mirrored = true,
			iconborder = true,
		}
		firstTime = true
	end
	db = CooldownWatchVar
	self:Debug(1,'Initialize()', GetAddOnMetadata('CooldownWatch', 'Version'))

	do
		local f = CreateFrame("Frame","CooldownWatchFrame",UIParent)
		-- Bug fix: At the 2. frame after a /rl, the elapsed parameter is bogus, which leads to wrong cooldowns.
		-- This seems to be a flaw in Blizz code.
		local numframestowait = 10
		local numframes = 0
		local function onupdatebugfix(_, elapsed)
			numframes=numframes+1
			if numframestowait>1 then
				numframestowait=numframestowait-1
				if elapsed>5 then 
					numframestowait = 1 
				end
				return
			end
			--CooldownWatch:Print('checking after',numframes,'frames')
			for i,f in ipairs(frame) do 
				f.Stop() 
				f.CheckCooldown() 
			end
			f:SetScript('OnUpdate', nil)
		end
		f:SetScript('OnUpdate', onupdatebugfix)
	end
	self:UpdateLoadData()

	do
		local cmd = function(msg)
			if msg:sub(1,5) == 'debug' then
				local lvl = tonumber(msg:sub(7))
				self:EnableDebug(lvl)
			elseif msg:sub(1,3)=='log' then
				if not self.debuglog then
					self:Print('no debug log found')
				else
					--error(self.debuglog)
					-- create an ugly frame so that ppl can copy the log
					local logframe = CreateFrame("Frame", nil, UIParent)
					logframe:SetWidth(300) logframe:SetHeight(24) 
					logframe:SetPoint('CENTER')

					local btn = CreateFrame("Button",nil, logframe,'UIPanelButtonTemplate')
					btn:SetWidth(24)
					btn:SetHeight(24)
					btn:SetPoint('LEFT')
					btn:SetText('x')
					btn:SetScript("OnClick", function() logframe:Hide() end)

					local editbox = CreateFrame("EditBox",nil,logframe,'InputBoxTemplate')
					editbox:SetMultiLine(true)
					editbox:SetPoint("TOPLEFT",25,0)
					editbox:SetPoint("BOTTOMRIGHT")
					editbox:SetText(self.debuglog)
					editbox:HighlightText()
					logframe:Show()
					logframe:Raise()
				end
			else
				self:ToggleConfigMode() 
			end
		end
		SlashCmdList["COOLDOWNWATCH"] = cmd
		setglobal("SLASH_COOLDOWNWATCH1", "/COOLDOWNWATCH")
		setglobal("SLASH_COOLDOWNWATCH2", "/CW")
	end

	if CooldownWatch:IsDebugEnabled() then
		setglobal("f",frame)
		setglobal("cw",self)
		setglobal("db",db)
	end

	if firstTime then
		CooldownWatch:FirstTime()
	end
	CooldownWatch.initialized = true
end


-------------------------------------------------------------------------------------------------------------------------------- 
-- module Init
function CooldownWatch.modules.Init.init(...)
function CooldownWatch:Enable()
	self:StartEventListening()
	self:LoadData()
	self:UnloadInit()
end

function CooldownWatch:UpdateLoadData()
	self.UpdateLoadData = nil

	if not db.version then
		-- ignore the update code, watches have to be recreated.
		CooldownWatchInit.updatecode = nil
		db.version = 1
	end
	if db.version == 1 then
		CooldownWatchInit.updatecode = nil
		db.width = db.width+db.height
		db.version = 2
	end
	if db.version == 2 then
		CooldownWatchInit.updatecode = nil
		db.version = 3
	end
	if db.version == 3 then
		db.frameData = CooldownWatchInit.frameData
		CooldownWatchInit = nil
		db.version = 4
	end
	if db.version == 4 then
		for i,data in ipairs(db.frameData) do
			local options = {
				nobar = data.nobar,
				label = data.label,
				autohide = data.autohide,
				autohidebar = data.autohidebar,
				duration = data.duration,
				remaining = data.remaining,
			}
			data.options = options
			data.nobar = nil
			data.label = nil
			data.autohide = nil
			data.autohidebar = nil
			data.duration = nil
			data.remaining = nil
		end
		db.version = 5
	end
	if db.version == 5 then
		db.mirrored = true
		db.version = 6
	end
	if db.version == 6 then
		db.iconborder = true
		db.version = 7
	end
	if db.version == 7 then
		db.spacing = 2
		db.version = 8
	end
end


function CooldownWatch:UnloadInit()
	self.LoadData = nil
	self.UpdateLoadData = nil
	self.FirstTime = nil
	self.Initialize = nil
	self.Enable = nil
	self.UnloadInit = nil

	if self.modules.Config.status=='not loaded' then
		-- unload InitConfig
		self.CreateWatchFrame = nil
		self.AddWatch = nil
		self.GetOnUpdate = nil
		self.UnloadInit = nil
		modules.InitConfig.status = 'not loaded'
	end
end

function CooldownWatch:FirstTime()
	self:Print('Usage:')
	local function p(s) DEFAULT_CHAT_FRAME:AddMessage(s) end
	p([[Enter /cooldownwatch or /cw to toggle the config mode. A small anchor frame is visible if you're in config mode. Since this is the first time you're using CooldownWatch, the config mode is enabled now. Normally you will have to use the slash command.]])
	p([[Move the cooldown frames: Drag the anchor.]])
	p([[Add a cooldown: Drag and drop a spell or item to the anchor frame to add it at the end.]])
	p([[Or drop it on another cooldown to insert it at that position, and use that cooldown's settings.]])
	p([[Note: If you drag an item from your bags, the item itself will be added as the cooldown. If you drag an item from your character frame, the equipment slot it came from will be watched. That way, you can watch for example the cooldowns of the trinkets you're currently wearing.]])
	p([[Remove a cooldown: Middle click the cooldown frame.]])
	p([[Reorder cooldowns: Drag the cooldown frame.]])
	p([[Access cooldown specific options: Right-click the cooldown frame.]])
	p([[Access global options: Right-click the anchor frame.]])
	self:ConfigMode(true)
	p([[IMPORTANT: While you're in config mode, cooldowns won't get updated. You have to leave config mode in order to make cooldowns work.]])
end

function CooldownWatch:LoadData()
	if db.debug then
		self:EnableDebug(db.debug)
	end

	self:SetSpacing(db.spacing)
	CooldownWatchAnchor.layout = db.layout
	if not CooldownWatchFont:SetFont(db.font, db.fontsize, 'OUTLINE') then
		self:Print(format('|cffff2222Warning|r Font "%s" not found. Did you delete an addon? Reverting to default.',db.font))
		db.font = 'Fonts\\FRIZQT__.TTF'
		CooldownWatchFont:SetFont('Fonts\\FRIZQT__.TTF', db.fontsize, 'OUTLINE')
	end
	CooldownWatchFont:SetTextColor(unpack(db.textcolor))
	CooldownWatchFont:SetShadowColor(unpack(db.textshadowcolor))

	if db.frameData then
		self:Debug(1,'creating frames')
		for frameid,data in ipairs(db.frameData) do
			self:AddWatch(data.watchtype, data.watchname, data.options)
		end
	end
	if db.position then
		local p=db.position
		CooldownWatchAnchor:SetPoint(p.point, UIParent, p.relativePoint,p.x,p.y)
	end
end
end -- Init.init

-------------------------------------------------------------------------------------------------------------------------------- 

function CooldownWatch:SaveData()
	db.debug = CooldownWatch:IsDebugEnabled()
	if self.configmode then
		self:ConfigMode(false)
	end
	if not CooldownWatch.initialized then
		return
	end

	local point,parent,relativePoint,x,y = CooldownWatchAnchor:GetPoint(1)
	db.position = {point=point, relativePoint=relativePoint, x=x,y=y}
end

do
	-- Addon Initialization
	CooldownWatchAnchor:SetScript('OnEvent', function(frame, event, ...)
		if event == 'VARIABLES_LOADED' then
			CooldownWatch:Initialize()
			frame:UnregisterEvent('VARIABLES_LOADED')
		elseif event == 'PLAYER_ENTERING_WORLD' then
			CooldownWatch:Enable()
			frame:UnregisterEvent('PLAYER_ENTERING_WORLD')
		elseif event == 'PLAYER_LOGOUT' then
			CooldownWatch:SaveData()
			frame:UnregisterEvent('PLAYER_LOGOUT')
		elseif event == 'PLAYER_TALENT_UPDATE' then
			CooldownWatch:CheckTalents()
		end
	end)
	CooldownWatchAnchor:RegisterEvent('VARIABLES_LOADED')
	CooldownWatchAnchor:RegisterEvent('PLAYER_ENTERING_WORLD')
	CooldownWatchAnchor:RegisterEvent('PLAYER_LOGOUT')
	CooldownWatchAnchor:RegisterEvent('PLAYER_TALENT_UPDATE')
	function CooldownWatch:Printf(s,...)
		DEFAULT_CHAT_FRAME:AddMessage(format("|cff33ff99CooldownWatch|r: "..s,...))
	end
	function CooldownWatch:Print(s)
		DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99CooldownWatch|r: "..s)
	end
	function CooldownWatch:EnableDebug(lvl)
		if lvl==nil or type(lvl)=='number' then
			db.debug = lvl
			if lvl == nil then
				self:Print("Debugging disabled.")
			else
				self:Printf("Debug level set to %d.", lvl)
			end
		end
	end
	local function stringify(arg, ...)
		local s
		if type(arg)=="string" then
			s = '"'..arg..'"'
		elseif type(arg) == 'nil' then
			s='nil'
		else
			s = tostring(arg)
		end
		if select('#',...)>0 then
			return s, stringify(...)
		else
			return s
		end
	end
	function CooldownWatch:Debug(lvl, ...)
		if lvl and db and db.debug and db.debug>=lvl then
			local msg = strjoin(', ',stringify(...))
			DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99cw|r: "..msg)
			if self.debuglog then
				self.debuglog = self.debuglog .. '\n'..msg
			else
				self.debuglog = msg
			end
		end
	end
	function CooldownWatch:IsDebugEnabled(...)
		return db.debug
	end
	function CooldownWatch:Trace(...)
		self:Debug(3,...)
	end
end
